feat(backend): pluggable stem-separation backend (Demucs ↔ MSST/BS-RoFormer) - #141
Conversation
…Former) Add a selectable separation backend so the Demucs stems stage can be swapped for the stronger MSST/BS-RoFormer models from SUC-DriverOld/MSST-WebUI, driving MSSeparator inference directly (no PySide6 GUI). Default stays Demucs; the stems JSON contract (vocals/bass/drums/other, 44.1 kHz) is unchanged. - separation_backend.py: ASA_SEPARATION_BACKEND dispatcher + model registry, mirroring loudness_backend.py (env-select, graceful degrade-to-Demucs on any failure). MSST runs as a subprocess under its own venv (ASA_MSST_PYTHON) so it works on the worker path (server.py hardcodes ./venv), keeps MSST stdout off analyze.py's JSON contract, and isolates MSST's conflicting deps. - scripts/msst_separate_runner.py: runs in the MSST venv; resamples to 44.1 kHz, transposes channels-last→first, maps stems by name (SCNet order varies), forces MSST logging to stderr, emits a one-line JSON manifest. - analyze.py: route all three separation sites (--separate, --pitch-note-only, --mt3-only) through separate_stems_backend. - separation_ab.py + scripts/ab_separation_backends.py: research-only A/B harness — synthetic gain-aligned SI-SDR smoke-test (labelled plumbing, not a quality ranking) plus optional real-track reference-free proxies and fair runtime. - requirements-msst.txt: separate-venv setup pointer (MSST brings its own torch). - tests/test_separation_backend.py: 16 tests (dispatch, fallback, registry, runner helpers) — no MSST install required. - Docs: CLAUDE.md env vars + core-files + scripts + dep-isolation; JSON_SCHEMA.md notes the backend is selectable and the schema backend-agnostic. Default path unchanged: test_analyze (147) and test_phase1_golden (11) green. https://claude.ai/code/session_01PCXgSHYKWnrD4PiRkwzJxR
slittycode
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Summary
Adds a pluggable stem-separation backend (ASA_SEPARATION_BACKEND=msst) via subprocess isolation — the right architectural call given MSST's dep conflicts with the product venv. The default path is an untouched pass-through to Demucs; the stems JSON contract (vocals/bass/drums/other, 44.1 kHz PCM16) is unchanged. Frontend and WASM CI passed; Backend CI is still running (Essentia install is slow), but the PR description's test counts (16 new + 147 existing + 11 golden) are credible given the change doesn't touch any DSP logic.
Findings
Worth considering (neither blocking):
-
No happy-path MSST dispatch test.
DispatchTestscovers: Demucs default, MSST missing env → fallback, subprocess nonzero exit → fallback.RunnerManifestParseTestscovers manifest parsing on success. What's missing is a test exercising the full chain:separate_stems_backend→_separate_via_msst_subprocess→ subprocess exits 0 with valid JSON → stems returned. The individual pieces are tested in isolation but the integration isn't. Low effort to add — mock subprocess to return exit 0 + a valid manifest JSON, mockos.path.isfileto return True, assertseparate_stems_backendreturns the expected stems dict. -
30-minute subprocess timeout (
separation_backend.py,subprocess.run(..., timeout=1800)). When MSST hangs rather than failing cleanly (e.g., stalled on GPU memory, blocked weight download), the analysis worker blocks for up to 30 minutes beforeTimeoutExpiredpropagates and triggers the Demucs fallback. Acceptable for a default-off experiment; worth knowing before enabling in production.
Test results
Frontend: pass. Loudness WASM: pass. Backend: in progress. PR-reported counts (16/16 test_separation_backend, 147/147 test_analyze, 11/11 test_phase1_golden) are consistent with the diff scope.
Phase boundary check
Clean. separation_backend.py and msst_separate_runner.py are Phase 1 plumbing only. No Phase 2/3 involvement. No Phase 1 schema fields added, removed, renamed, or retyped. stemAnalysis schema note correctly updated to "backend-agnostic". Tripwire #1 (stdout purity): contextlib.redirect_stdout(sys.stderr) wraps the entire MSST execution block in the runner; json.dump(manifest, sys.stdout) happens after the context exits. All three separate_stems call sites in analyze.py correctly migrated to separate_stems_backend.
Generated by Claude Code
Add an integration test exercising the full success path: separate_stems_backend -> _separate_via_msst_subprocess -> subprocess exits 0 with a valid manifest -> stems returned with no Demucs fallback. The pieces were tested in isolation; this closes the end-to-end gap (PR #141 review). https://claude.ai/code/session_01PCXgSHYKWnrD4PiRkwzJxR
…/B + licence gate (#144) Follows PR #141's pluggable separation backend. Confirms the licence position (the campaign's stated prerequisite), makes the MSST runner work end-to-end, and adds a ground-truth-stems SDR mode so Demucs-vs-MSST can be measured on real isolated stems. Default path untouched: ASA_SEPARATION_BACKEND still defaults to demucs; analyze.py's JSON contract is unchanged. Licence gate (previously unconfirmed): - MSST-WebUI code is AGPL-3.0; both registry checkpoints (scnet_4stem, bs_roformer_vocals) are CC-BY-NC-SA-4.0 (NonCommercial). That is a strictly worse posture than the (MUSDB-grey) Demucs incumbent, so the MSST backend is research / NonCommercial-only and NOT promotable to a commercial default -- promotion is licence-gated, not quality-gated. - Durable in-code guardrails: LICENCE GATE notes on _MSST_MODEL_REGISTRY and in requirements-msst.txt. Full map + rationale + A/B results: incorporations/msst-separation-licence-gate-2026-06-05.md msst_separate_runner.py -- 3 integration fixes (never exercised before; PR #141 had no MSST install): - chdir into the checkout (MSSeparator import reads data_backup/webui_config.json via a relative path); caller paths resolved to absolute first so they survive. - drop the injected stdlib logger (MSSeparator relies on MSST's own get_logger with a console_handler attribute); its handler targets stderr and the existing redirect_stdout keeps the stdout JSON contract clean. - sample-rate read: regex fallback for configs carrying !!python/tuple tags. separation_ab.py -- ground-truth reference-set mode (--ref-dir): loads MUSDB-style {mixture,vocals,bass,drums,other}.wav, runs each backend, scores true per-stem SI-SDR + aggregate; warmup-skip for the subprocess MSST backend (it reloads per call). New tests/test_separation_ab.py (loader + aggregation + si_sdr). Existing separation tests stay green. NonCommercial research A/B (5 MUSDB18 test tracks, 7s, CPU, mono gain-aligned SI-SDR -- a preliminary proxy, NOT museval): Demucs 8.08 dB @1.3s vs scnet_4stem 6.22 dB @40.8s (~32x slower on CPU). MSST wins vocals (10.77 vs 8.62) but trails overall; this contradicts published museval SDR, so the short-clip proxy is the limiter -- a full-length re-run is the honest basis for any quality claim. The gate conclusion is unchanged either way: do not promote. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: sync AGENTS.md files against post-#141 codebase Two files drifted when the separation backend landed in #141 (which updated CLAUDE.md but not the per-app AGENTS.md overlays) and when patchSmith landed in #130. apps/backend/AGENTS.md: - Add separation_backend.py entry (selectable Demucs ↔ MSST backend) - Add separation_ab.py entry (research-only A/B harness) - Add tests/test_separation_backend.py entry - Add msst_separate_runner.py and ab_separation_backends.py to the Operator and Research Scripts section apps/ui/AGENTS.md: - Add patchSmith.ts entry (Phase 3 Vital preset generation service) No code changes; CLAUDE.md was already complete after #141. https://claude.ai/code/session_011v17uVcfV8rM9EQpRn72Ra * docs: fix patchSmith.ts description in apps/ui/AGENTS.md The initial entry incorrectly said the preset builder uses Phase 2 recommendations. patchSmith.ts imports only Phase1Result — every parameter cites a Phase 1 field per PURPOSE.md invariant #2. Correct the description to reflect that. Caught by review on PR #142. https://claude.ai/code/session_011v17uVcfV8rM9EQpRn72Ra --------- Co-authored-by: Claude <noreply@anthropic.com>
What & why
ASA's stem-separation stage was hard-wired to torchaudio Hybrid Demucs. This adds a pluggable separation backend so it can be swapped for the stronger MSST/BS-RoFormer models from
SUC-DriverOld/MSST-WebUI, drivingMSSeparatorinference directly (no PySide6 GUI). The default stays Demucs and the stems JSON contract is unchanged (vocals/bass/drums/other, 44.1 kHz), so thestemAnalysisoverlay, pitch/note translation, and MT3 all behave identically.Selected with
ASA_SEPARATION_BACKEND=msst(defaultdemucs). Any failure degrades gracefully back to Demucs — this is a default-off experiment, mirroringloudness_backend.py.Design: subprocess, not in-process import
MSST runs as a subprocess under its own venv (
ASA_MSST_PYTHON) rather than an in-process import. This was the key architectural finding from a 4-agent design review, and it solves five problems at once:server.pyalways invokesanalyze.pyunder./venv/bin/python, so an in-process MSST import could never see MSST's deps (it'd be dead code on the product path).analyze.py's stdout JSON (tripwire feat: value-first backport — genre profiles, MixDoctor, broken-pipe fixes #1).librosa==0.9.2,numpy<2, its own torch; they can't coexist with ASA'storch==2.10.0. The MSST venv is built from MSST-WebUI's ownrequirements.txt.utils/inferencepackages don't shadow ASA's.del_cache()runs in the runner'sfinally.Changes
New
apps/backend/separation_backend.py— dispatcher + model registry (ASA_MSST_MODEL, defaultscnet_4stem) + subprocess driverapps/backend/scripts/msst_separate_runner.py— runs in the MSST venv: resamples to 44.1 kHz, transposes channels-last→first, maps stems by name (SCNet emits[drums,bass,other,vocals]), stderr-only logging, JSON manifestapps/backend/separation_ab.py+scripts/ab_separation_backends.py— research-only A/B harness: synthetic gain-aligned SI-SDR smoke-test (explicitly labelled plumbing, not a real-music quality ranking — RoFormer is trained on real spectra) + optional real-track reference-free proxies + fair runtime (warm-up, min-of-repeats, device recorded)apps/backend/requirements-msst.txt— separate-venv setup pointerapps/backend/tests/test_separation_backend.py— 16 tests (no MSST install needed)Edited
apps/backend/analyze.py— route all three separation sites (--separate,--pitch-note-only,--mt3-only) throughseparate_stems_backendCLAUDE.md,apps/backend/JSON_SCHEMA.md— env vars, core-files, scripts, dep-isolation; schema note that separation is backend-agnosticCorrectness notes (from the review)
sonic_analyzer_demucs_prefix socleanup_stemsreclaims them (no leak).analyze_loudness(stereo, sample_rate=44_100)in_run_per_stem_analyses— otherwise per-stem LUFS would be silently wrong.bs_roformer_vocalsregistry entry is research/A-B-only (leaves bass/drums empty); the defaultscnet_4stemfills all four.Verification
test_separation_backend— 16 passedtest_analyze— 147 passed (EXPECTED_TOP_LEVEL_KEYSunchanged)test_phase1_golden— 11 passed (golden snapshot unchanged)MSST end-to-end requires an MSST-WebUI checkout + 4-stem checkpoint (see
requirements-msst.txt); the subprocess boundary is mocked in tests so CI needs no MSST install.https://claude.ai/code/session_01PCXgSHYKWnrD4PiRkwzJxR
Generated by Claude Code